Skip to content

Speed up CI: fix NY tax-benefit-system cloning, run batches concurrently, rebalance to 14 runners#8886

Merged
MaxGhenis merged 6 commits into
PolicyEngine:mainfrom
hua7450:ci-speedup
Jul 5, 2026
Merged

Speed up CI: fix NY tax-benefit-system cloning, run batches concurrently, rebalance to 14 runners#8886
MaxGhenis merged 6 commits into
PolicyEngine:mainfrom
hua7450:ci-speedup

Conversation

@hua7450

@hua7450 hua7450 commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

CI took ~34 min wall-clock across 17 runners, sat at the memory cliff (single batches measured at 12-14.6 GB on 16 GB runners — the source of the historical intermittent "runner lost communication" failures), and needed hand-rearranging as tests grew. This PR fixes the worst per-case cost bug, re-batches the suite from measured per-batch peak RSS, and adds permanent timing/memory telemetry to every CI log.

Final state (validated by 3 full runs on this PR): all green, zero OOMs, ~26 min wall on 23 full-suite runners, every subprocess ≤ ~8.5 GB peak — half of each runner's memory left as headroom so newly added heavy tests don't require re-tuning.

Fixes #8114

Changes

1. NY #8114 fix (policyengine_us/tools/pinned_tbs.py + 3 NY credit variables)

The NY EITC / pre-2024 CTC formulas deep-cloned the entire tax-benefit system per formula call. Each pinned system (pre-ARPA 2020 EITC, pre-TCJA 2017 CTC) is now built once per process in a module-level cache keyed on the baseline system's identity (weakref — layered reforms rebuild against their own base). Measured: ny_ctc_pre_2024.yaml 373s → 57s, ny_eitc.yaml 106s → 53s, peak RSS 4.8 → 2.8 GB, outputs identical. The dedicated states-ny CI runner is retired; NY runs in the ordinary states shards.

2. Permanent CI telemetry

  • Every job sets PYTEST_ADDOPTS="--durations=25 -p no:unraisableexception -p no:threadexception" — each log ends with the 25 slowest test cases, and subprocesses skip pytest 8.4's ~11s full-heap gc sweep at exit.
  • test_batched.py reports each batch subprocess's true peak RSS (VmHWM from /proc) in its footer and the summary. Heavy new tests become visible in green runs, not red ones.
  • (test_batched.py also gained an opt-in --workers N pool; CI uses 2 only for the Partners job, whose batches max at 2.5 GB. Everything else runs single-worker after measurement showed reform-heavy batches co-scheduled 2-wide exceed 16 GB.)

3. Memory-headroom re-batching (measured, not estimated)

Per-batch peak RSS from this PR's own runs drove the layout: states --batches 16 across 4 shards, household --batches 4, gov/simulation and baseline/contrib/states per-file, oh joins ri in contrib per-file isolation, congress/irs/ssa heavy batches run strictly solo. Two files were too big for any batching and are split by reform-parameter combination with byte-identical cases: contrib/states/ri/ctc_reform_test.yaml (14.6 GB alone) → 3 files; contrib/crfb/tax_employer_payroll_tax_percentage.yaml (9.6 GB) → 2 files.

4. Process-lifetime memory bugs in test runners

  • The combined make test-other pytest process OOM'd at 94%: microsimulation tests now run as their own CI job, and test_microsim.py builds one Microsimulation per dataset (module-scoped fixture; 2 builds instead of 4 — it was the real ~17-min hotspot of the old Rest job). The RUN_HEAVY_TESTS LSR/CG tests (skipped on CI) now subsample and share a baseline.
  • run_selective_tests.py (Quick Feedback) packed every selected location into one process — reuniting the very files split above and OOMing under coverage. It now runs one location per subprocess (coverage run -a appends across invocations), bounding memory at the heaviest single location.

5. Job layout: 17 → 23 runners

Baseline: states-shard-1..4, irs, household, ssa-usda, rest-a, rest-b, contrib-hhs, reform. Contrib: states-shard-1..4, other-shard-1, 2a, 2b, 3, congress. Plus Partners, Rest (Python + variables), Microsimulation. Total runner-minutes are roughly unchanged — the compute is spread wider and shorter, with growth headroom on every runner.

Measured results (run on commit 1d4e2b9)

All 35 checks green. Wall 26m27s (was ~33.7 min). Old→new highlights: Baseline rest 33.7 → 12.0∥10.6; Rest+microsim 29.8 → 11.8∥11.7; irs-household 23.6 → 14.5∥10.9; Partners 29.8 → 22.2; contrib other max 26.4 → 14.1; reform 9.6 → 9.4. Worst job: contrib states-shard-4 (26m27s — carries the RI per-file batches).

Notes for maintainers

  • Required status checks need updating: deleted Baseline (states-ny/states-non-ny-*/rest/contrib/ssa/irs-household), Reform (per-file), Contrib (other-shard-1/2/3 as previously named); new names per the layout above.
  • A natural follow-up (not in this PR): pack the states shards from a measured-duration cost ledger instead of alphabetical stride — contrib states-shard-4 carries a disproportionate content draw and sets the wall; balanced packing projects ~22-23 min walls.
  • Every log now shows per-batch peak RSS: if any batch trends toward ~10 GB in future green runs, split it then — no more surprise OOMs.

Test plan

  • 3 full CI runs on this PR; final run entirely green (35/35 checks)
  • Per-batch peak RSS verified ≤ ~8.5 GB across all jobs
  • NY fix outputs verified byte-identical before/after
  • Split test files verified case-count-identical (RI 5+4+2=11; CRFB 3+3=6)

🤖 Generated with Claude Code

@hua7450

hua7450 commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator Author

Round 2 pushed — re-batched from the measured per-batch peak RSS that round 1's new logging produced.

Round 1 result: 9/15 heavy jobs passed with large wins (rest 33.7→10.4 min, Partners 29.8→18.0, irs-household 23.6→13.9), but 2-wide batch co-scheduling OOM'd four jobs: the RSS telemetry revealed single batches already peak at 12-14.6 GB on Linux (ri/ctc_reform_test.yaml = 14.6 GB as one file; contrib/states/oh 12.4 GB; gov/simulation 12.1 GB) — the pre-existing layout was living at the cliff, which explains the historical intermittent "runner lost communication" failures.

Round 2 is headroom-first, per maintainer direction (long-term robustness to new tests > minimum runner count):

  • --workers 1 everywhere except Partners (max batch 2.5 GB measured)
  • Finer batches so every subprocess stays ≤ ~8 GB: states --batches 16 × 4 shards, household --batches 4, gov/simulation + baseline/contrib/states per-file, oh joins ri in per-file isolation
  • The two files no batching can save are split by reform combo (cases relocated byte-identically): RI CTC → 3 files, CRFB payroll sweep → 2 files
  • Microsimulation pytest gets its own job (the combined test-other process was OOM-killed at 94%)
  • Quick Feedback selective cap 250→100 files (over-cap safely defers to full suites)

Now 23 full-suite runners (was 17 before this PR), each with ≥ ~8 GB of slack for future heavy tests, and every log permanently reports per-batch peak RSS + the 25 slowest cases so drift is visible before it breaks.

@codecov

codecov Bot commented Jul 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (f9e58e7) to head (685408a).
⚠️ Report is 102 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff            @@
##              main     #8886   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            3         3           
  Lines           55        99   +44     
  Branches         0        10   +10     
=========================================
+ Hits            55        99   +44     
Flag Coverage Δ
unittests 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@MaxGhenis

Copy link
Copy Markdown
Contributor

Data point for this rebalance from tonight's merge queue: "Full Suite - Contrib (states-shard-2)" was killed (exit 143 / runner-canceled, ~17-26 min in) on at least nine separate PR runs across #8679, #8818, #8901 (twice), #8902, #8903 (twice), #8907, and #8908 — every one passed unchanged on rerun, so it's load/memory, not tests. That shard looks like the first candidate for whatever split this PR lands on.

hua7450 and others added 6 commits July 5, 2026 11:21
The NY EITC and pre-2024 CTC formulas deep-cloned the entire
tax-benefit system (parameter tree + variable registry) on every call
that hit the decoupled/pre-TCJA branch, driving the NY credits test
memory to ~12 GB and requiring a dedicated quarantined CI runner.

Build each pinned system (pre-ARPA 2020 EITC, pre-TCJA 2017 CTC) once
per process in a module-level cache keyed on the baseline system's
identity, so layered reforms still rebuild against their own base.
Measured on the credits test files: ny_ctc_pre_2024.yaml 373s -> 57s,
ny_eitc.yaml 106s -> 53s, peak RSS 4.8 -> 2.8 GB, outputs identical.

Fixes PolicyEngine#8114

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New --workers N flag (default 1 = unchanged sequential behavior) runs
batch subprocesses concurrently, longest-first by YAML file count, with
per-batch output buffered and printed atomically plus a 60s heartbeat.
Each batch now reports its subprocess peak RSS (VmHWM from /proc on
Linux) so CI logs show real per-batch memory. Grace sleep after pytest
completion reduced from 5s to 1s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With the NY clone fix, states-ny folds back into the states matrix
(8 batches, 2 shards). Reform and hhs merge into the baseline-contrib
runner, usda moves to the ssa runner, contrib other-shards 1+3 merge,
and the partners job fans analytics_coverage/edge_cases out per
topic/state folder (invocation-only; partner files untouched). Heavy
targets run batches 2-wide, the light rest job 3-wide. All test jobs
set PYTEST_ADDOPTS to print the 25 slowest cases and skip pytest's
unraisableexception gc sweep (~11s per subprocess).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
test_microsim.py now builds one Microsimulation per dataset via a
module-scoped fixture shared across the parametrized years (2 full
builds instead of 4; ~17 min of the Rest CI job). The RUN_HEAVY_TESTS
LSR/CG interaction tests (still skipped by default) subsample to
10,000 households and reuse a single baseline, so opting in is cheaper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round-1's --workers 2 co-scheduling OOM-killed four runners: the new
per-batch RSS logging showed single batches already peak far higher on
Linux than local estimates suggested (ri/ctc_reform_test.yaml 14.6 GB
alone, contrib/states/oh 12.4 GB, baseline/contrib/states 12.2 GB,
gov/simulation 12.1 GB, half of baseline/household 11.8 GB).

Headroom-first re-layout: every heavy target runs one batch at a time
(--workers 2 kept only for partners, max batch 2.5 GB), oversized
batches are split finer (states --batches 16 over 4 shards, household
--batches 4, gov/simulation and baseline/contrib/states per-file, oh
joins ri in PER_FILE_STATES), the two files too big for any batching
are split by reform combo (RI CTC into 3, CRFB payroll into 2, cases
byte-identical), and the microsimulation pytest run gets its own job
since the combined process was OOM-killed at 94%. Every subprocess now
stays at or below ~8 GB, leaving half the 16 GB runner free for future
test growth. Quick Feedback's selective cap drops to 100 files (one
subprocess with 250 yaml files exceeds runner memory; over-cap safely
defers to the full suites).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Quick Feedback OOM'd twice: run_selective_tests packed every selected
location into one process (this PR selected the whole NY folder plus
all RI and CRFB reform files), where reformed tax-benefit systems and
per-case simulations accumulate for the process lifetime — exceeding
runner memory under coverage even though each location alone is fine.
One subprocess per location bounds memory at the heaviest single
location; coverage -a appends results across invocations unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@MaxGhenis MaxGhenis marked this pull request as ready for review July 5, 2026 21:49

@MaxGhenis MaxGhenis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fable review + instrumented verification: the pinned-TBS cache replaces per-call full-system clones in the NY pre-TCJA CTC and pre-ARPA EITC formulas (12→2 clones, peak shard RSS 7.30→3.72 GB, byte-identical outputs), with weakref identity keying so reformed systems rebuild rather than leak baseline parameters. Shard rebalance proven exhaustive: 4,280/4,280 test files covered, zero orphans/dups. This addresses the exit-143 kills that hit ~20 runs in the last day. CI green.

@MaxGhenis MaxGhenis merged commit 4e6b6a8 into PolicyEngine:main Jul 5, 2026
35 checks passed
hua7450 added a commit that referenced this pull request Jul 6, 2026
Folder-per-batch with a hand-maintained heavy-state list OOM'd within a
day of PR #8886: two merged PRs (#8816, #8856) added reform test files
to 13 state folders, and the GA batch silently grew from 3 to 11
distinct reform combos. Each distinct (reforms, parameter-overrides)
combination deep-copies the full tax-benefit system into
policyengine-core's never-evicted cache (~1.45 GB each, calibrated
against 46 contrib batches on CI run 28756084246), so that one
subprocess peaked at 15.2 GB on 16 GB runners and killed contrib
states-shard-4 on most runs after 2026-07-05 23:00 UTC.

- test_batched.py now packs each state's files into batches capped at
  reform-combo weight 5 (~8.7 GB predicted peak), replacing the
  PER_FILE_STATES hand-list and the RI shard-pinning special case —
  both subsumed by the general mechanism.
- Split the four over-budget files (OR/DE at 7 combos, OH/MD at 6) into
  two files each by combo group; cases relocated byte-identically.
- New code-health test caps the combo weight one test file may carry,
  so future over-budget files fail at authoring time instead of
  OOM-ing CI later.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NY EITC/CTC formulas clone the full tax-benefit system on every call — CI memory blowup

2 participants